import {
    AwaitMatch,
    AwaitReport,
    AwaitWhen,
    DefineRuleContext,
    DslAdapter,
    DslRule,
    EngineCapabilityHint,
    NodePredicate,
    PropType,
    RuleContext, WhenResult,
} from 'qvog-dsl';
import { BaseChecker } from '../checker/BaseChecker';
import { AstMatcher, MatcherCallback, MatcherTypes, StmtMatcher } from '../matcher/Matchers';
import { getHcEngineCapability, postInitHcEngineCapability } from './Capability';

import { HcEngineCapability, HcHint, HcIssue, HcIssueTemplate, HcRuleContext } from './Defines';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { execSync } from 'child_process';
import path from 'path';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'Adapter');

type CheckerBuilderAction<C> = (builder: CheckerBuilder<C>, context: C) => void;

class CheckerBuilder<C> {
    private init: CheckerBuilderAction<C> = () => {
    };
    private actions: CheckerBuilderAction<C>[] = [];
    private predicate: (node: any) => boolean = () => false;
    private callback: ((node: any) => void) = undefined!;

    constructor() {
    }

    setInit(init: CheckerBuilderAction<C>): void {
        this.init = init;
    }

    setPredicate(predicate: (node: any) => boolean): void {
        this.predicate = predicate;
    }

    setCallback(callback: (node: any) => void): void {
        this.callback = callback;
    }

    addAction(action: CheckerBuilderAction<C>): void {
        this.actions.push(action);
    }

    build(): DslRule<BaseChecker> {
        return (capability: HcEngineCapability): BaseChecker => {
            const checker: BaseChecker = {
                rule: undefined!,
                registerMatchers: undefined!,
                check: () => {},
                issues: []
            };

            checker.registerMatchers = (): MatcherCallback[] => {
                // FIXME: This context is incomplete. Capability is inferred from the
                // user's options.
                let context: HcRuleContext = {
                    options: { use: 'ast' },
                } as HcRuleContext;

                this.init(this, context as C);
                capability = getHcEngineCapability(context.options?.use || 'ast');
                context = {
                    ...context,
                    ...capability
                };

                // The initialization order is messy, however, `registerMatchers`
                // is sure to be called once and only once before the matcher
                // is ready to be used. Therefore, we can initialize context-aware
                // properties here.
                postInitHcEngineCapability(capability, checker);

                this.actions.forEach((action) => {
                    action(this, context as C);
                });
                if (context.options?.use === 'ast') {
                    const matcher: AstMatcher = {
                        matcherType: MatcherTypes.AST,
                        match: this.predicate,
                    };
                    const callback: MatcherCallback = {
                        matcher: matcher,
                        callback: this.callback
                    };
                    return [callback];
                } else {
                    const matcher: StmtMatcher = {
                        matcherType: MatcherTypes.STMT,
                        match: this.predicate,
                    }
                    const callback: MatcherCallback = {
                        matcher: matcher,
                        callback: this.callback
                    };
                    return [callback];
                }
            };

            return checker;
        };
    }
}

class AwaitMatchAdapter<H extends HcHint, C extends HcRuleContext> implements AwaitMatch<H, C> {
    private readonly builder: CheckerBuilder<C>;

    constructor(builder: CheckerBuilder<C>) {
        this.builder = builder;
    }

    match<T extends PropType<H, 'node'>>(param: NodePredicate<T> | ((node: PropType<H, 'node'>, context: C) => node is T)): AwaitWhen<PropType<H, 'node'>, T, C> {
        if (typeof param === 'function') {
            this.matchWithAction(param);
        } else {
            this.matchWithPredicate(param);
        }
        return new AwaitWhenAndReportAdapter<PropType<H, 'node'>, T, C>(this.builder);
    }

    private matchWithPredicate<T extends PropType<H, 'node'>>(predicate: NodePredicate<T>): void {
        this.matchWithAction((node): node is T => predicate.is(node));
    }

    private matchWithAction<T extends PropType<H, 'node'>>(predicate: (node: PropType<H, 'node'>, context: C) => node is T): void {
        this.builder.addAction((builder, context) => {
            builder.setPredicate((node) => {
                return predicate(node, context);
            });
        });
    }
}

class AwaitWhenAndReportAdapter<N, T, C extends HcRuleContext> implements AwaitWhen<N, T, C>, AwaitReport<N, C> {
    private builder: CheckerBuilder<C>;
    // By default, `when` is always true to make this step optional.
    private whenAction: (node: T, context: C) => WhenResult<N> = () => true;

    constructor(builder: CheckerBuilder<C>) {
        this.builder = builder;
    }

    when(action: (node: T, context: C) => WhenResult<N>): AwaitReport<N, C> {
        this.whenAction = action;
        return this;
    }

    report(param: ((node: N, context: C) => void) | HcIssueTemplate): DslRule<BaseChecker> {
        if (typeof param === 'function') {
            this.reportImplAction(param);
        } else {
            this.reportImplTemplate(param);
        }
        return this.builder.build();
    }

    private reportImplAction(action: (node: N, context: C) => void): void {
        this.builder.addAction((builder, context) => {
            builder.setCallback((node: any) => {
                const result = this.whenAction(node as T, context);
                if (result === undefined || result === null) {
                    // nothing to report
                } else if (typeof result === 'boolean') {
                    if (result) {
                        action(node, context);
                    }
                } else if (Array.isArray(result)) {
                    result.forEach(n => action(n, context));
                } else {
                    action(result, context);
                }
            });
        });
    }

    private reportImplTemplate(template: HcIssueTemplate): void {
        this.reportImplAction((node, { api, native }) => {
            const location = api.getLocation(node);
            const issue: HcIssue = {
                location: location,
                severity: template.severity ?? native.rule.alert,
                description: template.description,
                docPath: template.docPath
            };
            api.report(issue);
        });
    }
}

function entry<P, H extends EngineCapabilityHint, U extends DefineRuleContext<P, H>>(
    init: (context: RuleContext<H>) => P
): AwaitMatch<H, U> {
    const builder = new CheckerBuilder<U>();
    builder.setInit((_, context) => {
        context.props = init(context);
    });
    return new AwaitMatchAdapter(builder);
}

export class HcAdapter extends DslAdapter<BaseChecker> {
    public constructor() {
        super();
    }

    entry = entry;

    loadDslRule(dslRule: DslRule<BaseChecker>): BaseChecker {
        // Capability will be deducted on initialization.
        return dslRule(undefined!);
    }

    loadDslRuleFromFileAsync(file_path: string): BaseChecker {
        const dirName = path.basename(path.dirname(file_path));
        const baseName = path.basename(file_path, path.extname(file_path));
        const command = `npx tsc "${file_path}" \
            --target es2016 \
            --module commonjs \
            --declaration true \
            --declarationMap false \
            --esModuleInterop true \
            --forceConsistentCasingInFileNames true \
            --strictPropertyInitialization false \
            --strict true \
            --skipLibCheck true \
            --experimentalDecorators true \
            --outDir ./out/${dirName}`;
        try {
            execSync(command, {
                encoding: 'utf-8',           // 返回字符串而不是 Buffer
            });
        } catch (e: any) {
            if (e.status !== 0) {
                // 真正的编译错误
                logger.error(`❌ ${file_path} 编译失败: ${e.stderr || e.stdout}`);
            }
        }
        const targetPath = path.join(process.cwd(), 'out', dirName, baseName + '.js');
        const module = require(targetPath);
        const dslRule = module.default;
        return this.loadDslRule(dslRule);
    }
}

// `adapter` should be imported before any DSL file, because
// DSL indirectly depends on `adapter` to get properly initialized.
export const adapter = new HcAdapter();
